<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Feed_Pubsubhubbub
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Callback.php 23775 2011-03-01 17:25:24Z ralph $
 */

/**
 * @see Zend_Feed_Pubsubhubbub
 */
require_once 'Zend/Feed/Pubsubhubbub.php';

/**
 * @see Zend_Feed_Pubsubhubbub
 */
require_once 'Zend/Feed/Pubsubhubbub/CallbackAbstract.php';

/**
 * @see Zend_Feed_Reader
 */
require_once 'Zend/Feed/Reader.php';

/**
 * @category   Zend
 * @package    Zend_Feed_Pubsubhubbub
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Feed_Pubsubhubbub_Subscriber_Callback extends Zend_Feed_Pubsubhubbub_CallbackAbstract {
	/**
	 * Contains the content of any feeds sent as updates to the Callback URL
	 *
	 * @var string
	 */
	protected $_feedUpdate = null;
	
	/**
	 * Holds a manually set subscription key (i.e. identifies a unique
	 * subscription) which is typical when it is not passed in the query string
	 * but is part of the Callback URL path, requiring manual retrieval e.g.
	 * using a route and the Zend_Controller_Action::_getParam() method.
	 *
	 * @var string
	 */
	protected $_subscriptionKey = null;
	
	/**
	 * After verification, this is set to the verified subscription's data.
	 *
	 * @var array
	 */
	protected $_currentSubscriptionData = null;
	
	/**
	 * Set a subscription key to use for the current callback request manually.
	 * Required if usePathParameter is enabled for the Subscriber.
	 *
	 * @param  string $key
	 * @return Zend_Feed_Pubsubhubbub_Subscriber_Callback
	 */
	public function setSubscriptionKey($key) {
		$this->_subscriptionKey = $key;
		return $this;
	}
	
	/**
	 * Handle any callback from a Hub Server responding to a subscription or
	 * unsubscription request. This should be the Hub Server confirming the
	 * the request prior to taking action on it.
	 *
	 * @param  array $httpGetData GET data if available and not in $_GET
	 * @param  bool $sendResponseNow Whether to send response now or when asked
	 * @return void
	 */
	public function handle(array $httpGetData = null, $sendResponseNow = false) {
		if ($httpGetData === null) {
			$httpGetData = $_GET;
		}
		
		/**
		 * Handle any feed updates (sorry for the mess :P)
		 *
		 * This DOES NOT attempt to process a feed update. Feed updates
		 * SHOULD be validated/processed by an asynchronous process so as
		 * to avoid holding up responses to the Hub.
		 */
		$contentType = $this->_getHeader ( 'Content-Type' );
		if (strtolower ( $_SERVER ['REQUEST_METHOD'] ) == 'post' && $this->_hasValidVerifyToken ( null, false ) && (stripos ( $contentType, 'application/atom+xml' ) === 0 || stripos ( $contentType, 'application/rss+xml' ) === 0 || stripos ( $contentType, 'application/xml' ) === 0 || stripos ( $contentType, 'text/xml' ) === 0 || stripos ( $contentType, 'application/rdf+xml' ) === 0)) {
			$this->setFeedUpdate ( $this->_getRawBody () );
			$this->getHttpResponse ()->setHeader ( 'X-Hub-On-Behalf-Of', $this->getSubscriberCount () );
		/**
		 * Handle any (un)subscribe confirmation requests
		 */
		} elseif ($this->isValidHubVerification ( $httpGetData )) {
			$data = $this->_currentSubscriptionData;
			$this->getHttpResponse ()->setBody ( $httpGetData ['hub_challenge'] );
			$data ['subscription_state'] = Zend_Feed_Pubsubhubbub::SUBSCRIPTION_VERIFIED;
			if (isset ( $httpGetData ['hub_lease_seconds'] )) {
				$data ['lease_seconds'] = $httpGetData ['hub_lease_seconds'];
			}
			$this->getStorage ()->setSubscription ( $data );
		/**
		 * Hey, C'mon! We tried everything else!
		 */
		} else {
			$this->getHttpResponse ()->setHttpResponseCode ( 404 );
		}
		if ($sendResponseNow) {
			$this->sendResponse ();
		}
	}
	
	/**
	 * Checks validity of the request simply by making a quick pass and
	 * confirming the presence of all REQUIRED parameters.
	 *
	 * @param  array $httpGetData
	 * @return bool
	 */
	public function isValidHubVerification(array $httpGetData) {
		/**
		 * As per the specification, the hub.verify_token is OPTIONAL. This
		 * implementation of Pubsubhubbub considers it REQUIRED and will
		 * always send a hub.verify_token parameter to be echoed back
		 * by the Hub Server. Therefore, its absence is considered invalid.
		 */
		if (strtolower ( $_SERVER ['REQUEST_METHOD'] ) !== 'get') {
			return false;
		}
		$required = array ('hub_mode', 'hub_topic', 'hub_challenge', 'hub_verify_token' );
		foreach ( $required as $key ) {
			if (! array_key_exists ( $key, $httpGetData )) {
				return false;
			}
		}
		if ($httpGetData ['hub_mode'] !== 'subscribe' && $httpGetData ['hub_mode'] !== 'unsubscribe') {
			return false;
		}
		if ($httpGetData ['hub_mode'] == 'subscribe' && ! array_key_exists ( 'hub_lease_seconds', $httpGetData )) {
			return false;
		}
		if (! Zend_Uri::check ( $httpGetData ['hub_topic'] )) {
			return false;
		}
		
		/**
		 * Attempt to retrieve any Verification Token Key attached to Callback
		 * URL's path by our Subscriber implementation
		 */
		if (! $this->_hasValidVerifyToken ( $httpGetData )) {
			return false;
		}
		return true;
	}
	
	/**
	 * Sets a newly received feed (Atom/RSS) sent by a Hub as an update to a
	 * Topic we've subscribed to.
	 *
	 * @param  string $feed
	 * @return Zend_Feed_Pubsubhubbub_Subscriber_Callback
	 */
	public function setFeedUpdate($feed) {
		$this->_feedUpdate = $feed;
		return $this;
	}
	
	/**
	 * Check if any newly received feed (Atom/RSS) update was received
	 *
	 * @return bool
	 */
	public function hasFeedUpdate() {
		if ($this->_feedUpdate === null) {
			return false;
		}
		return true;
	}
	
	/**
	 * Gets a newly received feed (Atom/RSS) sent by a Hub as an update to a
	 * Topic we've subscribed to.
	 *
	 * @return string
	 */
	public function getFeedUpdate() {
		return $this->_feedUpdate;
	}
	
	/**
	 * Check for a valid verify_token. By default attempts to compare values
	 * with that sent from Hub, otherwise merely ascertains its existence.
	 *
	 * @param  array $httpGetData
	 * @param  bool $checkValue
	 * @return bool
	 */
	protected function _hasValidVerifyToken(array $httpGetData = null, $checkValue = true) {
		$verifyTokenKey = $this->_detectVerifyTokenKey ( $httpGetData );
		if (empty ( $verifyTokenKey )) {
			return false;
		}
		$verifyTokenExists = $this->getStorage ()->hasSubscription ( $verifyTokenKey );
		if (! $verifyTokenExists) {
			return false;
		}
		if ($checkValue) {
			$data = $this->getStorage ()->getSubscription ( $verifyTokenKey );
			$verifyToken = $data ['verify_token'];
			if ($verifyToken !== hash ( 'sha256', $httpGetData ['hub_verify_token'] )) {
				return false;
			}
			$this->_currentSubscriptionData = $data;
			return true;
		}
		return true;
	}
	
	/**
	 * Attempt to detect the verification token key. This would be passed in
	 * the Callback URL (which we are handling with this class!) as a URI
	 * path part (the last part by convention).
	 *
	 * @param  null|array $httpGetData
	 * @return false|string
	 */
	protected function _detectVerifyTokenKey(array $httpGetData = null) {
		/**
		 * Available when sub keys encoding in Callback URL path
		 */
		if (isset ( $this->_subscriptionKey )) {
			return $this->_subscriptionKey;
		}
		
		/**
		 * Available only if allowed by PuSH 0.2 Hubs
		 */
		if (is_array ( $httpGetData ) && isset ( $httpGetData ['xhub_subscription'] )) {
			return $httpGetData ['xhub_subscription'];
		}
		
		/**
		 * Available (possibly) if corrupted in transit and not part of $_GET
		 */
		$params = $this->_parseQueryString ();
		if (isset ( $params ['xhub.subscription'] )) {
			return rawurldecode ( $params ['xhub.subscription'] );
		}
		
		return false;
	}
	
	/**
	 * Build an array of Query String parameters.
	 * This bypasses $_GET which munges parameter names and cannot accept
	 * multiple parameters with the same key.
	 *
	 * @return array|void
	 */
	protected function _parseQueryString() {
		$params = array ();
		$queryString = '';
		if (isset ( $_SERVER ['QUERY_STRING'] )) {
			$queryString = $_SERVER ['QUERY_STRING'];
		}
		if (empty ( $queryString )) {
			return array ();
		}
		$parts = explode ( '&', $queryString );
		foreach ( $parts as $kvpair ) {
			$pair = explode ( '=', $kvpair );
			$key = rawurldecode ( $pair [0] );
			$value = rawurldecode ( $pair [1] );
			if (isset ( $params [$key] )) {
				if (is_array ( $params [$key] )) {
					$params [$key] [] = $value;
				} else {
					$params [$key] = array ($params [$key], $value );
				}
			} else {
				$params [$key] = $value;
			}
		}
		return $params;
	}
}
